Skip to content

JS: Report command injection through child_process argument arrays when shell is enabled - #22978

Open
vishalshetagar wants to merge 3 commits into
github:mainfrom
vishalshetagar:fix/js-shell-command-argument-injection
Open

vishalshetagar wants to merge 3 commits into
github:mainfrom
vishalshetagar:fix/js-shell-command-argument-injection

Conversation

@vishalshetagar

@vishalshetagar vishalshetagar commented Nov 2, 2026 •

Copy link
Copy Markdown

Description of the fix

js/command-line-injection now reports remote input that reaches the argument array of spawn/execFile when the shell option is set to true or a nonempty string, including their synchronous variants.

const cp = require("child_process");
const app = require("express")();

app.get("/a", (req, res) => {
  cp.exec("echo hello " + req.query.name); // reported
  res.send("ok");
});

app.get("/b", (req, res) => {
  cp.spawn("echo", ["hello", req.query.name], { shell: true }); // now reported
  res.send("ok");
});

app.get("/c", (req, res) => {
  cp.spawn("echo", ["hello", req.query.name], { shell: "/bin/sh" }); // now reported
  res.send("ok");
});

All three routes are now reported by js/command-line-injection. Argument arrays in these examples with shell: false, shell: "", or no shell option remain unreported.

For child_process.spawn, spawnSync, execFile, and execFileSync, the command-injection sink modeling reuses UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink. The sink is the argument array at the execution call, so array mutations must reach that call to be reported. executesArrayAsShell also recognizes nonempty string-valued shell paths and checks for local writes that overwrite the shell setting before execution.

The child_process model distinguishes argument arrays, options objects, and callbacks. It handles execFile calls with or without a callback, excludes options-only calls from argument-array sinks, and preserves js/useless-use-of-cat findings when callbacks cannot be resolved locally.

Testing

Regression fixtures and expected results cover boolean and string-valued shell options, synchronous and promisified calls, callback-free execFile, forwarded arrays/options, and push/unshift. Negative cases cover disabled, empty, omitted, and overwritten shell options, options-only calls, constant arguments, and array mutations after execution.

The CommandInjection, UnsafeShellCommandConstruction, UselessUseOfCat, and NodeJSLib test suites pass.

Fixes #22825.

Copilot AI auto review requested due to automatic review settings November 2, 2026 13:23
@vishalshetagar
vishalshetagar requested a review from a team as a code owner November 2, 2026 13:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@asgerf asgerf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks harmless to merge, but without any tests it's hard to be confident that this PR actually does what is claimed by the change note.

// the rest (exec/execFile) has the options argument as their second last.
result = this.getParameter(this.getNumArgument() - 2).asSink()
// exec/execFile may have a callback after the options argument.
result = this.getParameter([this.getNumArgument() - 2, this.getNumArgument() - 1]).asSink()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a line earlier in this predicate (line 807) that reads:

not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlist

This seems to be at odds with what this PR is claiming to solve. It seems that this filter should be disabled when shell=true.

It would also be appropriate with unit tests for this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The changes can produce false positives and suppress existing findings by misclassifying options and callbacks, counting post-execution array mutations, and honoring overwritten shell values, with regression coverage also missing.

5 open issues

Review effort: Max

// the rest (exec/execFile) has the options argument as their second last.
result = this.getParameter(this.getNumArgument() - 2).asSink()
// exec/execFile may have a callback after the options argument.
result = this.getParameter([this.getNumArgument() - 2, this.getNumArgument() - 1]).asSink()
Comment on lines +62 to +63
exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink |
this = sink and
Comment on lines +62 to +63
exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink |
this = sink and
node.asSink() = sys.getOptionsArg() and
node.getMember("shell").asSink().mayHaveBooleanValue(true)
exists(DataFlow::Node shell |
shell = sys.getOptionsArg().getALocalSource().getAPropertyWrite("shell").getRhs()
Comment on lines +62 to +63
exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink |
this = sink and
@vishalshetagar

Copy link
Copy Markdown
Author

Thanks @asgerf, I’ve pushed an update with regression tests and expected results.

The array filter is in getOptionsArg(), so I’ve kept it to distinguish the options object from the argument array. The new tests verify that getArgumentList() still recognizes arrays when shell is enabled.

I also addressed Copilot’s findings around unresolved callbacks, options-only calls, array mutations after execution, and overwritten shell settings. All four affected test suites pass. Would appreciate another look!

@vishalshetagar
vishalshetagar requested a review from asgerf November 5, 2026 21:25

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JS: js/command-line-injection misses child_process argument arrays when the shell option is set

3 participants